import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import java.util.*;

/**
 * Our TwoFourApp application Must have Two Things - a Screen And a GUI - I've
 * also added a third member Tree234 that holds all the data needed to render a 234
 * Tree and  animation. Some of the most intresting methods are the
 * update(deltaTime) methods, * the update method will be call constantly, so
 * the the world will be constantly updating itself and telling it's members to
 * update themselves.
 * 
 * 
 */
public class TwoFourApp implements Application {
	// the screen for this application - See Class Below, it's a JPanel.
	public Screen screen;
	// the GUI for this application - See Class Below
	public TheGUI gui;
	// TODO: Change this object reference to the appropriate thing.
	// TreePlaceHolder tree;
	Tree234<Integer> tree;

	private static final int WORLD_WIDTH = 800;
	private static final int WORLD_HEIGHT = 500;
	// Default space between edge of drawing area center of closest node
	private static final int TREE_BORDER_OFFSET = 20;
	private static final int ELEMENT_MAX = 100;

	/**
	 * used for the fixed-time-step simulation
	 */
	static final float TICK_DECREMENT = 0.05f;

	/**
	 * used for the fixed-time-step simulation - used to adjust the speed of
	 * animation
	 */
	static float TICK_INITIAL = 0.003f;

	/**
	 * used for the fixed-time-step simulation - used to adjust the speed of
	 * animation
	 */
	float tickTime = 0;

	// used to drag the tree across the screen
	static float rootX, rootY;

	// used to scale the tree
	private double scaleX, scaleY;

	private boolean dirty;

	/**
	 * Create a new GUI object and ask the GUI for the Graphics screen we want
	 * to render
	 */
	public TwoFourApp()
	{
		dirty = false;
		gui = new TheGUI(this);
		screen = gui.getGraphicsScreen();
		// TODO: Replace this with the Relevant Tree Object.
		// tree = new TreePlaceHolder();
		tree = new Tree234<Integer>();
		rootX = WORLD_WIDTH / 2;
		rootY = 30;
		scaleX = 2;
		scaleY = 2;
	}

	/**
	 * Think of the screen as the World that the Growing circle lives in, the
	 * world should only be responsible for telling the growing circle to update
	 * it's self and then the screen will re draw it's self
	 * 
	 * @author Simeon Gbolo
	 * 
	 */
	class TwoFourScreen extends Screen {

		/**
		 * 
		 * @param app
		 *            The application running - must call the super class so we
		 *            know what screen to update
		 */
		public TwoFourScreen(Application app)
		{
			super(app);
			// TODO: Instantiate Tree Details here.

		}

		// MUST be called sets the screens default size
		// Automatically called when screen is created(thanks java)
		public Dimension getPreferredSize()
		{
			return new Dimension(WORLD_HEIGHT, WORLD_HEIGHT);
		}

		/**
		 * * Here we add the delta time to our accumulator. The while loop will
		 * use up as many ticks as have been accumulated (e.g., when tickTime is
		 * 1.2 and one tick should take 0.5 seconds, we can update the world
		 * twice, leaving 0.2 seconds in the accumulator). This is called a
		 * fixed-time-step simulation.
		 * 
		 * then we tell the tree to update it's self
		 */
		@Override
		public void update(float deltaTime)
		{
			tickTime += deltaTime;
			while (tickTime > TICK_INITIAL)
			{
				tickTime -= TICK_INITIAL;
				tree.update(deltaTime);
			}
		}

		/**
		 * Then repaint!
		 */
		@Override
		public void present(float deltaTime)
		{
			repaint();
		}

		@Override
		public void pause()
		{
			// TODO Auto-generated method stub
		}

		@Override
		public void paintComponent(Graphics g)
		{
			super.paintComponent(g);

			if (tree != null && !tree.isCleared())
			{
				Graphics2D g2d = (Graphics2D) g;

				g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
						RenderingHints.VALUE_ANTIALIAS_ON);

				g.setFont(new Font(Font.SERIF, Font.BOLD, 14));

				if (dirty)
				{
					// Set center coordinates for tree nodes
					tree.setLocation(new Point((int) (WORLD_WIDTH / 2),
							TREE_BORDER_OFFSET));

					// Minimize spread of tree
					tree.compressDrawWidth();

					// Move tree to minimum left extent
					tree.setLeftEdgeGap(TREE_BORDER_OFFSET);

					tree.reCalculateDestinations();
					dirty = false;
				}

				// translate to mouse position
				g2d.translate(rootX - ((Tree234.rootX) * scaleX), rootY);

				// scale the tree accordingly
				g2d.scale(scaleX, scaleY);

				// tree.drawTree(g2d);
				tree.drawStates(g2d);
			}
		}
	}

	/**
	 * All the GUI for the application
	 * 
	 * @author Simeon Gbolo
	 * @modifier Jay Ripley
	 * 
	 */
	class TheGUI extends JPanel implements ActionListener, MouseMotionListener,
			ChangeListener {
		// Canvas for Animation and Drawing
		private TwoFourScreen graphics;
		// Buttons
		private HashMap<String, JButton> buttons;
		// JPanels used for later rendering.
		JPanel textInfoPanel, inputPanel, compositePanel, sliderPanel;
		// Input Field - used by Event Listeners
		JTextField jtfInput;
		// Output Field - used by Event Listeners
		JTextArea jtaOutput;

		// used to adjust the size of the tree
		JSlider resizeSlider;

		// used to adjust the size of the tree
		JSlider animationSpeedSlider;

		public TheGUI(TwoFourApp animationTest1)
		{
			/*
			 * Primary Interface Panels and Widgets
			 */
			graphics = new TwoFourScreen(animationTest1);
			graphics.setBackground(Color.WHITE);
			textInfoPanel = new JPanel();
			sliderPanel = new JPanel();
			resizeSlider = new JSlider(1, 100, 100);
			animationSpeedSlider = new JSlider(1, 25, 15);
			resizeSlider.addChangeListener(this);
			animationSpeedSlider.addChangeListener(this);
			inputPanel = new JPanel();
			buttons = new HashMap<String, JButton>();

			jtfInput = new JTextField();
			jtfInput.setColumns(15);

			jtaOutput = new JTextArea(30, 10);
			jtaOutput.setEditable(false);
			jtaOutput.setLineWrap(true);
			jtaOutput.setWrapStyleWord(true);

			// Used as Structural Guidance
			compositePanel = new JPanel(new BorderLayout());

			createAndShowGUI();
		}

		/**
		 * Instantiate all the GUI objects
		 */
		private void createAndShowGUI()
		{
			/*
			 * Assemble the Button UI
			 */

			inputPanel.add(new JLabel("Value(s): "));
			inputPanel.add(jtfInput);

			// Insert Button
			buttons.put("Insert", new JButton("Insert"));
			// Find Button
			buttons.put("Find", new JButton("Find"));
			// Insert Button
			buttons.put("Delete", new JButton("Delete"));
			// Find Button
			buttons.put("Clear", new JButton("Clear Tree"));
			// Test button for generated insert
			buttons.put("Test", new JButton("Test"));

			// Setup an action listener onto all of the newly created buttons.
			Iterator<String> keys = buttons.keySet().iterator();
			JButton current_button;

			while (keys.hasNext())
			{
				current_button = buttons.get(keys.next());
				current_button.addActionListener(this);
				inputPanel.add(current_button);
			}

			// add mouse listeners to graphics
			graphics.addMouseMotionListener(this);
			/*
			 * Assemble the Text Info UI
			 */
			LineBorder jtaBorder = new LineBorder(Color.BLACK, 1);
			TitledBorder jtaTitledBorder = new TitledBorder(jtaBorder,
					"Output: ");

			textInfoPanel.setBorder(jtaTitledBorder);
			textInfoPanel.add(jtaOutput);

			// set border for the slider
			LineBorder sliderBorder = new LineBorder(Color.BLACK, 1);
			TitledBorder sliderTitledBorder = new TitledBorder(sliderBorder,
					"Scale tree size");

			// set border for the slider
			LineBorder sliderBorder2 = new LineBorder(Color.BLACK, 1);
			TitledBorder sliderTitledBorder2 = new TitledBorder(sliderBorder2,
					"Adjust Animation Speed");

			resizeSlider.setBorder(sliderTitledBorder);
			animationSpeedSlider.setBorder(sliderTitledBorder2);
			sliderPanel.add(resizeSlider);
			sliderPanel.add(animationSpeedSlider);

			/*
			 * Assemble the Composite UI (Graphics and Text Field)
			 */
			compositePanel.add(graphics, BorderLayout.CENTER);
			compositePanel.add(textInfoPanel, BorderLayout.EAST);

			/*
			 * Assemble Parent UI.
			 */
			setLayout(new BorderLayout());
			add(sliderPanel, BorderLayout.NORTH);
			add(compositePanel, BorderLayout.CENTER);
			add(inputPanel, BorderLayout.SOUTH);
		}

		/**
		 * This action even method takes the appropriate action depending on
		 * what action was performed. Uses
		 * if(e.getsource().equals(someObjectWithListeners) The appropriate
		 * action will be taken.
		 */
		public void actionPerformed(ActionEvent e)
		{
			String error_log = "";

			// Input Values array
			Integer[] input_ints = null;
			;

			// Act based on source.
			if (e.getSource().equals(buttons.get("Insert")))
			{
				System.out.println("You hit Insert");

				input_ints = parseNodeData();

				if (input_ints != null)
				{
					insertElements(input_ints);

					dirty = true;
				} else
				{
					error_log = "Unable to parse input value(s). "
							+ " Please try again.";
				}
			}
			// TODO For testing purposes only
			else
				if (e.getSource().equals(buttons.get("Test")))
				{
					// TODO Temporary button for mass list (testing)
					Integer int_count, test_int;
					ArrayList<Integer> int_list = new ArrayList<Integer>();

					try
					{
						int_count = Integer.parseInt(jtfInput.getText());
						// Build list of integers
						for (int i = 0; i < int_count; i++)
						{
							// Generate a random int, and test for it in list
							do
							{
								test_int = (int) (ELEMENT_MAX * Math.random());
							} while (int_list.contains(test_int));

							int_list.add(test_int);
						}
						jtfInput.setText(int_list.toString());
					} catch (Exception ex)
					{
						// Trap any errors
						System.out.println("Test Error");
					}
				} else
					if (e.getSource().equals(buttons.get("Find")))
					{
						System.out.println("You hit Find.");

						input_ints = parseNodeData();

						if (input_ints != null)
						{
							findElements(input_ints);
						} else
						{
							error_log = "Unable to parse input value(s). "
									+ " Please try again.";
						}
					} else
						if (e.getSource().equals(buttons.get("Delete")))
						{
							System.out.println("You hit delete.");

							input_ints = parseNodeData();

							if (input_ints != null)
							{
								if(tree.search(input_ints[0])){
								deleteElements(input_ints);
								dirty = true;
								}else{
									error_log += ""+input_ints[0] +" is not in the tree!" ;
									
								}
							} else
							{
								error_log = "Unable to parse input value(s). "
										+ " Please try again.";
							}
						} else
							if (e.getSource().equals(buttons.get("Clear")))
							{
								System.out.println("You hit clear.");

								tree.clear();
								// Clear input text field
								jtfInput.setText(null);
							}

			// Final Error Details
			if (error_log != "")
			{
				error_log += "\n---\n\n";
			}

			// Output Results
			jtaOutput.setText(error_log + tree.toString());
		}

		/**
		 * 
		 * @return The screen graphics
		 * 
		 */
		public Screen getGraphicsScreen()
		{
			return this.graphics;
		}

		/**
		 * Inserts multiple elements into the tree
		 * 
		 * @param node_els
		 *            - an array of Integer tree elements
		 */
		private void insertElements(Integer[] node_els)
		{
			// Ignore empty input
			if (node_els != null)
			{
				// Insert nodes for each item in array
				for (int i = 0; i < node_els.length; i++)
				{

					try
					{
						if (node_els[i] < ELEMENT_MAX)
						{
							tree.insert(node_els[i]);
						}
					}
					// Insert method throws DuplicateItemException
					catch (Exception ex)
					{
					}
				}
			}
		}

		/**
		 * Remove multiple elements from the tree
		 * 
		 * @param node_els
		 *            - an array of Integer tree elements
		 */
		private void deleteElements(Integer[] node_els)
		{
			// Ignore empty input
			if (node_els != null)
			{
				// Delete any node matching an item in the array
				for (int i = 0; i < node_els.length; i++)
				{

					// Find first node in item array
					try
					{
						tree.delete(node_els[i]);
					}
					// Delete method throws ItemNotFoundException
					catch (Exception ex)
					{
					}
				}
			}
		}

		/**
		 * Find multiple elements within the tree
		 * 
		 * @param node_els
		 *            - an array of Integer tree elements
		 */
		private void findElements(Integer[] node_els)
		{

			// Ignore empty input
			if (node_els != null)
			{
				// Find nodes for each item in array
				for (int i = 0; i < node_els.length; i++)
				{

					// Find each node in item array
					tree.search(node_els[i]);
				}
			}
		}

		/**
		 * Parses multiple Integer inputs from application text field, for use
		 * in tree operations
		 * 
		 * @author johnsdacgi@gmail.com (Daniel Johnson)
		 * 
		 * @return an array of parsed Integers, or null
		 */
		private Integer[] parseNodeData()
		{
			ArrayList<Integer> node_ints;
			String current_int = "";
			StringBuffer text_input;

			if (!jtfInput.getText().isEmpty())
			{
				node_ints = new ArrayList<Integer>();
				text_input = new StringBuffer(jtfInput.getText());

				while (text_input.length() > 0)
				{
					// Construct integer from numeric chars
					if (Character.isDigit(text_input.charAt(0)))
					{
						// Begin new integer string
						if (current_int == "")
						{
							current_int = text_input.substring(0, 1);
						}
						// Append to existing integer string
						else
						{
							current_int += text_input.substring(0, 1);
						}
					}
					// Store constructed integer when non-numeric char is found
					else
						if (current_int != "")
						{
							node_ints.add(Integer.parseInt(current_int));
							current_int = "";
						}

					// Remove processed (leading) character
					text_input.deleteCharAt(0);
				}

				// Store last constructed integer (if any), when loop terminates
				if (current_int != "")
				{
					node_ints.add(Integer.parseInt(current_int));
					current_int = "";
				}

				// Clear last input
				jtfInput.setText(null);

				// Check if no integers were parsed
				if (node_ints.isEmpty())
				{
					return null;
				}

				// Convert ArrayList to array
				return node_ints.toArray(new Integer[node_ints.size()]);
			}

			return null;
		}

		@Override
		public void mouseDragged(MouseEvent e)
		{
			// set the root of the tree to the x location of mouse
			rootX = e.getX();
			// set root of the tree to y location f the mouse
			rootY = e.getY();
		}

		@Override
		public void mouseMoved(MouseEvent arg0)
		{
			// TODO Auto-generated method stub

		}

		/**
		 * Used to adjust animation speed and scale the tree (scale not
		 * implemented for this tree
		 */
		@Override
		public void stateChanged(ChangeEvent arg0)
		{

			double sliderVal = resizeSlider.getValue();
			scaleX = sliderVal * .03;
			scaleY = sliderVal * .03;

			int speed = animationSpeedSlider.getValue();
			TICK_INITIAL = ((float) speed * 2 * .0001f);

		}
	}

	@Override
	public TheGUI getTheGui()
	{
		return gui;
	}

	/**
	 * Return the current screen
	 */
	@Override
	public Screen getCurrentScreen()
	{
		return this.screen;
	}

	/**
	 * Set the screen to a different screen
	 */
	@Override
	public void setScreen(Screen screen)
	{
		this.screen = screen;
	}
}
